package com.dbflow5.query;

import com.dbflow5.SqlUtils;
import com.dbflow5.StringUtils;
import com.dbflow5.config.FlowManager;
import com.dbflow5.converter.TypeConverters;
import com.dbflow5.data.Blob;
import com.dbflow5.query.property.Property;
import com.dbflow5.sql.Query;

/**
 * Description: Base class for all kinds of [SQLOperator]
 */
public abstract class BaseOperator implements SQLOperator {
    protected NameAlias nameAlias;

    /**
     * The operation such as "=", "&lt;", and more
     */
    protected String operation = "";

    /**
     * The value of the column we care about
     */
    protected Object value = null;

    /**
     * A custom SQL statement after the value of the Operator
     */
    protected String postArg = null;

    /**
     * An optional separator to use when chaining these together
     */
    protected String separator = null;

    /**
     * If true, the value is set and we should append it. (to prevent false positive nulls)
     */
    protected boolean isValueSet = false;

    public BaseOperator(NameAlias nameAlias){
        this.nameAlias = nameAlias;
    }

    /**
     * Object value
     *
     * @return the value of the argument
     */
    @Override
    public Object value() {
        return value;
    }

    /**
     * columnName
     *
     * @return the column name
     */
    @Override
    public String columnName() {
        if(nameAlias != null && nameAlias.getQuery() != null) {
            return nameAlias.getQuery();
        }
        return "";
    }

    @Override
    public SQLOperator separator(String separator) {
        this.separator = separator;
        return this;
    }

    @Override
    public String separator() {
        return separator;
    }

    /**
     * hasSeparator
     *
     * @return true if has a separator defined for this condition.
     */
    @Override
    public boolean hasSeparator() {
        return StringUtils.isNotNullOrEmpty(separator);
    }

    /**
     * operation
     *
     * @return the operator such as "&lt;", "&gt;", or "="
     */
    @Override
    public String  operation() {
        return operation;
    }

    /**
     * postArgument
     *
     * @return An optional post argument for this condition
     */
    public String postArgument() {
        return postArg;
    }

    /**
     * NameAlias
     *
     * @return internal alias used for subclasses.
     */
    public NameAlias columnAlias() {
        return nameAlias;
    }

    public String convertObjectToString(Object obj, boolean appendInnerParenthesis) {
        return convertValueToString(obj, appendInnerParenthesis);
    }

    public static String convertValueToString(Object value, boolean appendInnerQueryParenthesis) {
        return convertValueToString(value, appendInnerQueryParenthesis, true);
    }

    /**
     * Converts a value input into a String representation of that.
     *
     *
     * If it has a [TypeConverter], it first will convert it's value into its [TypeConverter.getDBValue].
     *
     *
     * If the value is a [Number], we return a string rep of that.
     *
     *
     * If the value is a [BaseModelQueriable] and appendInnerQueryParenthesis is true,
     * we return the query wrapped in "()"
     *
     *
     * If the value is a [NameAlias], we return the [NameAlias.getQuery]
     *
     *
     * If the value is a [SQLOperator], we [SQLOperator.appendConditionToQuery].
     *
     *
     * If the value is a [Query], we simply call [Query.getQuery].
     *
     *
     * If the value if a [Blob] or byte[]
     *
     * @param value                       The value of the column in Model format.
     * @param appendInnerQueryParenthesis if its a [BaseModelQueriable] and an inner query value
     * @param typeConvert typeConvert
     * @return Returns the result as a string that's safe for SQLite.
     */
    public static String convertValueToString(Object value,
                             boolean appendInnerQueryParenthesis,
                             boolean typeConvert) {
        if (value == null) {
            // Return to match NULL in SQLITE.
            return "NULL";
        } else {
            Object locVal = value;
            if (typeConvert) {
                TypeConverters.TypeConverter<?, Object> typeConverter = (TypeConverters.TypeConverter<?, Object>) FlowManager.getTypeConverterForClass(locVal.getClass());
                if (typeConverter != null) {
                    locVal = typeConverter.getDBValue(locVal);
                }
            }
            if (appendInnerQueryParenthesis && locVal instanceof BaseModelQueriable<?>) {
                return ((BaseModelQueriable)locVal).enclosedQuery();
            } else if (locVal instanceof Property<?> && locVal == Property.WILDCARD) {
                return locVal.toString();
            } else {
                if(locVal instanceof Number){
                    return locVal.toString();
                }else if(locVal instanceof Enum<?>){
                    return SqlUtils.sqlEscapeString(((Enum<?>) locVal).name());
                }else if(locVal instanceof NameAlias){
                    return ((NameAlias) locVal).getQuery();
                }else if(locVal instanceof SQLOperator){
                    StringBuilder queryBuilder = new StringBuilder();
                    ((SQLOperator) locVal).appendConditionToQuery(queryBuilder);

                    return queryBuilder.toString();
                }else if(locVal instanceof Query){
                    return ((Query) locVal).getQuery();
                }else if(locVal instanceof Blob || locVal instanceof byte[]){
                    byte[] bytes;
                    if(locVal instanceof Blob){
                        bytes = ((Blob) locVal).getBlob();
                    }else {
                        bytes = (byte[]) locVal;
                    }
                    return "X" + SqlUtils.sqlEscapeString(SqlUtils.byteArrayToHexString(bytes));
                }else {
                    return SqlUtils.sqlEscapeString(locVal.toString());
                }
            }
        }
    }

    /**
     * Returns a string containing the tokens joined by delimiters and converted into the property
     * values for a query.
     *
     * @param delimiter The text to join the text with.
     * @param tokens    an [Iterable] of objects to be joined. Strings will be formed from
     * @param condition condition
     * @return A joined string
     */
    public static String joinArguments(CharSequence delimiter, Iterable<?> tokens, BaseOperator condition) {
        StringBuilder builder = new StringBuilder();
        for(Object obj : tokens){
            String value = condition.convertObjectToString(obj, false);
            builder.append(value);
            builder.append(delimiter);
        }
        if(builder.length() > 0){
            builder.deleteCharAt(builder.length() - 1);
        }

        return builder.toString();
    }

    /**
     * Returns a string containing the tokens converted into DBValues joined by delimiters.
     *
     * @param delimiter The text to join the text with.
     * @param tokens    an array objects to be joined. Strings will be formed from
     * the objects by calling object.toString().
     * @return A joined string
     */
    public static String joinArguments(CharSequence delimiter, Object[] tokens) {
        StringBuilder builder = new StringBuilder();
        for(Object obj : tokens){
            String value = convertValueToString(obj, false, true);
            builder.append(value);
            builder.append(delimiter);
        }
        if(builder.length() > 0){
            builder.deleteCharAt(builder.length() - 1);
        }

        return builder.toString();
    }

    /**
     * Returns a string containing the tokens converted into DBValues joined by delimiters.
     *
     * @param delimiter The text to join the text with.
     * @param tokens    an array objects to be joined. Strings will be formed from
     * the objects by calling object.toString().
     * @return A joined string
     */
    public static String joinArguments(CharSequence delimiter, Iterable<?> tokens) {
        StringBuilder builder = new StringBuilder();
        for(Object obj : tokens){
            String value = convertValueToString(obj, false, true);
            builder.append(value);
            builder.append(delimiter);
        }
        if(builder.length() > 0){
            builder.setLength(builder.length() - delimiter.length());
        }
        return builder.toString();
    }
}
